package org.glandais.profiler.ui;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Font;
import java.awt.Paint;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.ObjectInputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.swing.AbstractAction;
import javax.swing.DefaultComboBoxModel;
import javax.swing.JComboBox;
import javax.swing.JFileChooser;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.filechooser.FileFilter;
import javax.swing.filechooser.FileNameExtensionFilter;

import org.glandais.profiler.Agent;
import org.glandais.profiler.Messages;
import org.glandais.profiler.beans.MethodTrace;
import org.glandais.profiler.beans.TraceStack;
import org.jfree.chart.ChartColor;
import org.jfree.chart.ChartFactory;
import org.jfree.chart.ChartPanel;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.axis.SymbolAxis;
import org.jfree.chart.labels.ItemLabelAnchor;
import org.jfree.chart.labels.ItemLabelPosition;
import org.jfree.chart.labels.XYItemLabelGenerator;
import org.jfree.chart.labels.XYToolTipGenerator;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.chart.plot.XYPlot;
import org.jfree.chart.renderer.xy.StandardXYBarPainter;
import org.jfree.chart.renderer.xy.XYBarRenderer;
import org.jfree.data.xy.XYDataset;
import org.jfree.data.xy.XYIntervalSeries;
import org.jfree.data.xy.XYIntervalSeriesCollection;
import org.jfree.ui.ApplicationFrame;
import org.jfree.ui.TextAnchor;

import com.thoughtworks.xstream.XStream;

public class AnalyzerFrame extends ApplicationFrame {

	private static final String SANS_SERIF = "SansSerif"; //$NON-NLS-1$

	private static final String EMPTY = ""; //$NON-NLS-1$

	private static final long serialVersionUID = 860295082395611490L;

	private static File lastDirectory = null;

	private static class LoadAction extends AbstractAction {

		private static final String XML = "xml"; //$NON-NLS-1$

		private static final long serialVersionUID = -3101036390027219974L;

		private AnalyzerFrame analyzerFrame;

		private static final FileFilter filter = new FileNameExtensionFilter(
				Messages.getString("AnalyzerFrame.XML_FILES"), XML); //$NON-NLS-1$

		public LoadAction(AnalyzerFrame analyzerFrame, String string) {
			super(string);
			this.analyzerFrame = analyzerFrame;
		}

		@Override
		public void actionPerformed(ActionEvent e) {
			final JFileChooser fc = new JFileChooser();
			fc.setFileFilter(filter);

			if (lastDirectory != null) {
				fc.setCurrentDirectory(lastDirectory);
			} else {
				fc.setCurrentDirectory(new File(Agent.configBean.getDirectory()));
			}

			int returnVal = fc.showOpenDialog(analyzerFrame);
			if (returnVal == JFileChooser.APPROVE_OPTION) {
				analyzerFrame.loadFile(fc.getSelectedFile());
				lastDirectory = fc.getSelectedFile().getParentFile();
			}
		}

	}

	public ArrayList<TraceStack> stacks = new ArrayList<TraceStack>();

	private XStream xstream;

	private JComboBox stackComboBox;

	private ChartPanel chartpanel;

	private StacksComboBoxModel comboboxModel;

	private static class StacksComboBoxModel extends DefaultComboBoxModel {

		private static final long serialVersionUID = 6288171713248457040L;

		private AnalyzerFrame analyzerFrame;

		public StacksComboBoxModel(AnalyzerFrame analyzerFrame) {
			super();
			this.analyzerFrame = analyzerFrame;
		}

		@Override
		public Object getElementAt(int index) {
			return analyzerFrame.stacks.get(index);
		}

		@Override
		public int getIndexOf(Object anObject) {
			return analyzerFrame.stacks.indexOf(anObject);
		}

		@Override
		public int getSize() {
			return analyzerFrame.stacks.size();
		}

		public void fire() {
			fireContentsChanged(analyzerFrame, 0, getSize() - 1);
		}

	}

	@SuppressWarnings("unchecked")
	public void loadFile(File selectedFile) {
		try {
			BufferedInputStream bis = new BufferedInputStream(new FileInputStream(selectedFile));
			ObjectInputStream xOIS = xstream.createObjectInputStream(bis);
			Object readObject = xOIS.readObject();
			if (readObject instanceof ArrayList) {
				stacks = (ArrayList<TraceStack>) readObject;
			} else {
				stacks = new ArrayList<TraceStack>(1);
				stacks.add((TraceStack) readObject);
			}
			Collections.sort(stacks, new Comparator<TraceStack>() {
				@Override
				public int compare(TraceStack o1, TraceStack o2) {
					if (o1.stackStart > o2.stackStart)
						return 1;
					else if (o1.stackStart < o2.stackStart)
						return -1;
					return 0;
				}
			});
			comboboxModel.fire();
			if (stacks.size() == 1) {
				comboboxModel.setSelectedItem(stacks.get(0));
			}
		} catch (Exception e) {
			e.printStackTrace(System.err);
		}
	}

	public AnalyzerFrame(String title) {
		super(title);
		xstream = Agent.createXStream();

		JPanel panel = new JPanel();
		BorderLayout layout = new BorderLayout();
		panel.setLayout(layout);

		panel.add(BorderLayout.NORTH, getStackList());
		panel.add(BorderLayout.CENTER, getChartContainer());

		setContentPane(panel);

		JMenuBar menuBar = new JMenuBar();
		JMenu menu1 = new JMenu(Messages.getString("AnalyzerFrame.FILE")); //$NON-NLS-1$
		JMenuItem calculer = new JMenuItem(new LoadAction(this, Messages.getString("AnalyzerFrame.OPEN"))); //$NON-NLS-1$
		menu1.add(calculer);
		menuBar.add(menu1);
		setJMenuBar(menuBar);
	}

	private Component getChartContainer() {
		chartpanel = new ChartPanel(null);
		chartpanel.setMouseWheelEnabled(true);
		return chartpanel;
	}

	private Component getStackList() {
		comboboxModel = new StacksComboBoxModel(this);
		stackComboBox = new JComboBox(comboboxModel);
		stackComboBox.addActionListener(new ActionListener() {
			@Override
			public void actionPerformed(ActionEvent e) {
				if (stackComboBox.getSelectedItem() != null) {
					JFreeChart chart = produceChart((TraceStack) stackComboBox.getSelectedItem());
					chartpanel.setChart(chart);
				}
			}
		});
		return stackComboBox;
	}

	private JFreeChart produceChart(TraceStack stack) {
		XYIntervalSeriesCollection dataset = new XYIntervalSeriesCollection();
		Map<String, XYIntervalSeries> series = new HashMap<String, XYIntervalSeries>();
		int md = collectSeries(stack.entryMethod, series, stack.entryMethod.startSystemNanoTime, 1);
		Collection<XYIntervalSeries> allSeries = series.values();
		for (XYIntervalSeries intervalSeries : allSeries) {
			dataset.addSeries(intervalSeries);
		}

		JFreeChart jfreechart = ChartFactory.createXYBarChart(EMPTY, Messages.getString("AnalyzerFrame.TIME"), //$NON-NLS-1$
				false, EMPTY, dataset, PlotOrientation.HORIZONTAL, false, true, false);

		tweakRendering(md, jfreechart, dataset);

		return jfreechart;
	}

	private void tweakRendering(int md, JFreeChart jfreechart, XYIntervalSeriesCollection dataset) {
		XYPlot xyplot = (XYPlot) jfreechart.getPlot();
		xyplot.setRangePannable(true);
		xyplot.setBackgroundPaint(Color.lightGray);
		xyplot.setDomainGridlinePaint(Color.white);
		xyplot.setRangeGridlinePaint(Color.white);

		setYLabels(md, xyplot);

		XYBarRenderer xybarrenderer = (XYBarRenderer) xyplot.getRenderer();
		tweakRenderer(xybarrenderer);

		Paint[] paints = ChartColor.createDefaultPaintArray();
		for (int i = 0; i < dataset.getSeriesCount(); i++) {
			int hashCode = dataset.getSeriesKey(i).toString().hashCode();
			int color = Math.abs(hashCode) % paints.length;
			xybarrenderer.setSeriesPaint(i, paints[color]);
		}

		xyplot.setRenderer(xybarrenderer);
	}

	private void tweakRenderer(XYBarRenderer xybarrenderer) {
		StandardXYBarPainter xyBarPainter = new StandardXYBarPainter();
		xybarrenderer.setBarPainter(xyBarPainter);
		xybarrenderer.setDrawBarOutline(true);
		xybarrenderer.setShadowVisible(false);
		xybarrenderer.setUseYInterval(true);
		// xybarrenderer.set

		XYItemLabelGenerator labelGenerator = new XYItemLabelGenerator() {
			@Override
			public String generateLabel(XYDataset dataset, int series, int item) {
				return getText(dataset, series, item);
			}
		};
		XYToolTipGenerator toolTipGenerator = new XYToolTipGenerator() {
			@Override
			public String generateToolTip(XYDataset dataset, int series, int item) {
				return getText(dataset, series, item);
			}
		};
		xybarrenderer.setBaseToolTipGenerator(toolTipGenerator);
		xybarrenderer.setBaseItemLabelGenerator(labelGenerator);
		xybarrenderer.setBaseItemLabelFont(new Font(SANS_SERIF, 0, 8));
		xybarrenderer.setBaseItemLabelsVisible(true);
		ItemLabelPosition position = new ItemLabelPosition(ItemLabelAnchor.CENTER, TextAnchor.CENTER,
				TextAnchor.CENTER, 0);
		xybarrenderer.setBasePositiveItemLabelPosition(position);
		xybarrenderer.setBaseNegativeItemLabelPosition(position);
		xybarrenderer.setPositiveItemLabelPositionFallback(position);
		xybarrenderer.setNegativeItemLabelPositionFallback(position);
	}

	private void setYLabels(int md, XYPlot xyplot) {
		List<String> symbols = new ArrayList<String>(md);
		for (int i = 0; i < md; i++) {
			symbols.add(Integer.toString(i));
		}
		String[] symbolsArray = symbols.toArray(new String[md]);
		SymbolAxis symbolaxis = new SymbolAxis(Messages.getString("AnalyzerFrame.PROFILE_SHORT"), symbolsArray); //$NON-NLS-1$
		symbolaxis.setGridBandsVisible(false);
		xyplot.setDomainAxis(symbolaxis);
	}

	private int collectSeries(MethodTrace entryMethod, Map<String, XYIntervalSeries> series, long startNanoTime,
			int depth) {
		int md = depth;
		XYIntervalSeries serie = getSerie(entryMethod.signature, series);
		double d = depth;
		double yLow = (entryMethod.startSystemNanoTime - startNanoTime) / 1000000.0d;
		double yHigh = yLow + ((entryMethod.durationNano) / 1000000.0d);
		serie.add(d, d - 0.45, d + 0.45, yLow, yLow, yHigh);
		for (MethodTrace subMethod : entryMethod.subMethods) {
			md = Math.max(md, collectSeries(subMethod, series, startNanoTime, depth + 1));
		}
		return md;
	}

	private String getText(XYDataset dataset, int series, int item) {
		XYIntervalSeriesCollection dataSetCollection = (XYIntervalSeriesCollection) dataset;
		double duration = dataSetCollection.getEndYValue(series, item) - dataSetCollection.getStartYValue(series, item);
		long lduration = (long) duration;
		String serie = dataset.getSeriesKey(series).toString();
		String name = serie.substring(0, serie.indexOf('('));
		return name + "(" + lduration + " ms)"; //$NON-NLS-1$ //$NON-NLS-2$
	}

	private static XYIntervalSeries getSerie(String signature, Map<String, XYIntervalSeries> series) {
		XYIntervalSeries serie = series.get(signature);
		if (serie == null) {
			serie = new XYIntervalSeries(signature);
			series.put(signature, serie);
		}
		return serie;
	}

}
